package storyteller;

import java.io.File;
import java.util.ArrayList;
import java.util.Vector;

import com.flagstone.transform.FSBounds;
import com.flagstone.transform.FSColor;
import com.flagstone.transform.FSColorTable;
import com.flagstone.transform.FSDefineFont2;
import com.flagstone.transform.FSDefineObject;
import com.flagstone.transform.FSDefineShape2;
import com.flagstone.transform.FSDefineShape3;
import com.flagstone.transform.FSDefineSound;
import com.flagstone.transform.FSDefineText2;
import com.flagstone.transform.FSMovie;
import com.flagstone.transform.FSPlaceObject;
import com.flagstone.transform.FSPlaceObject2;
import com.flagstone.transform.FSRemoveObject;
import com.flagstone.transform.FSSetBackgroundColor;
import com.flagstone.transform.FSShowFrame;
import com.flagstone.transform.FSSolidFill;
import com.flagstone.transform.FSSolidLine;
import com.flagstone.transform.FSSound;
import com.flagstone.transform.FSStartSound;
import com.flagstone.transform.util.FSImageConstructor;
import com.flagstone.transform.util.FSShapeConstructor;
import com.flagstone.transform.util.FSSoundConstructor;
import com.flagstone.transform.util.FSTextConstructor;


public class FlashWriter
{

  //`````````````````````
  // STATIC VARIABLES
  //`````````````````````    
 
  private static String FONT_FACE= "Arial";   // font size for the image labels (in twips)
  private static int TEXT_FONT_SIZE = 1000;   // font size for the text frames (in twips)
  private static int LABEL_FONT_SIZE = 600;   // font size for the image labels (in twips)
  private static final int FRAME_RATE = 30;   // frame rate of the whole movie, in frames per second
  private static final int DEFAULT_SLIDE_DURATION = 2;    // duration in seconds of each image/text slide
  private static final int BACKGROUND_LAYER = 2;
  private static final int IMAGE_LAYER = 3;
  private static final int LABEL_LAYER = 4;
  //private static final int VIDEO_LAYER = 5;
  private static final int MOVIE_WIDTH = 20*800;
  private static final int MOVIE_HEIGHT = 20*600;
  private static final String BACKGROUND_COLOR_LABEL = "bgcolor";
  private static final String DEFAULT_BACKGROUND_COLOR = "black";
  
  
  //`````````````````````
  // MEMBER VARIABLES
  //`````````````````````
  private FSMovie _movie;
  private String _outputFilename;
  private FSBounds _maxBounds; 
  private FlashAnalyzer _analyzer;
  private FlashAction _currentAction;
  private String _currentBgColor = DEFAULT_BACKGROUND_COLOR;
  
  public ArrayList<String> Logger;  
  
  // List of objects needing to be removed from the movie
  private ArrayList<FSRemoveObject> pendingObjects = new ArrayList<FSRemoveObject>();  
  private Vector<Integer> currentAudioTracks = new Vector<Integer>();
  private int _activeBackground;
  
  
  //`````````````````````
  // CONSTRUCTOR
  //`````````````````````
  
  public FlashWriter(String outputFilename)
  {
    init(outputFilename);
  }
  
  private void init(String outputFilename)
  {
    _outputFilename = outputFilename;
    
    String tmpDirectory = (new File(_outputFilename)).getAbsoluteFile().getParent(); 
    _analyzer = new FlashAnalyzer(tmpDirectory,true);
    
    _movie = new FSMovie();
    _maxBounds = null;
    _currentAction = null;
    
    Logger = new ArrayList<String>();
  }
  
  //`````````````````````
  // INTERFACE
  //`````````````````````
  
  public String lastLog()
  {
    if (Logger.size() == 0)
    {
      return "";
    }
    else
    {
      return Logger.get(Logger.size() - 1);
    }
  }

  public void parseContent(String content)
  {
    _analyzer.analyzeAll(content,new File(_outputFilename));
  }
  
  public FlashAction[] parse(String filename) 
  {
    return _analyzer.parse(filename);
  }  
  
  public void save() 
  {    
    try
    {
      startMovie();
      FlashAction[] allActions = _analyzer.actions();

      setMovieBounds(allActions);
      insertMovieElements(allActions);

      endMovie();
      _movie.encodeToFile(_outputFilename);
      _analyzer.removeTemporaryFiles();
    } 
    catch (Exception e)
    {
      throw new RuntimeException("Unable to save swf file [" + _outputFilename + "]",e);
    }
  }


  public static FSBounds merge(FSBounds a, FSBounds b) 
  {
    
    if (a == null)
    {
      return b;
    }
    else if (b == null)
    {
      return a;
    }
    
    int newMinX = Math.min(a.getMinX(), b.getMinX());
    int newMinY = Math.min(a.getMinY(), b.getMinY());
    int newMaxX = Math.max(a.getMaxX(), b.getMaxX());
    int newMaxY = Math.max(a.getMaxY(), b.getMaxY());
    
    a.setMax(newMaxX, newMaxY);
    a.setMin(newMinX, newMinY);
    b.setMax(newMaxX, newMaxY);
    b.setMin(newMinX, newMinY);
    return a;
  }
  

  //`````````````````````
  // PRIVATE METHODS
  //`````````````````````  



  private void startMovie()
  {
    _movie.setFrameRate(FRAME_RATE);
    _movie.add(new FSSetBackgroundColor(FSColorTable.black()));    
    
    
    // Add a frame. TODO: delete
    //int identifier = _movie.newIdentifier();;
    /*
    FSShapeConstructor path = new FSShapeConstructor();
    path.add(new FSSolidLine(80, FSColorTable.olive()));
    path.add(new FSSolidFill(new FSColor(0,0,0,255)));
    path.rect(MOVIE_WIDTH/2, MOVIE_HEIGHT/2, MOVIE_WIDTH, MOVIE_HEIGHT);
    _movie.add(path.defineShape(identifier));
    _movie.add(new FSPlaceObject2(identifier, 1, 0, 0));
    _movie.add(new FSShowFrame());
    */
  }

  private void endMovie()
  {
    closePendingObjects();
    addStopAll();
  }
  
  /**
   * Adds a slide to the movie with the given image.
   * The slide will be shown for SLIDE_DURATION seconds. 
   * @param imageFilename the path of the image that will be shown
   */
  private void insertImageFrame(String imageFilename, boolean isChecking)
  {
    try {
      
      int myLayer = IMAGE_LAYER;
      
      FSImageConstructor imageGenerator = new FSImageConstructor(imageFilename);
      
      int imageId = _movie.newIdentifier();
      int shapeId = _movie.newIdentifier();
      
      FSDefineObject image = imageGenerator.defineImage(imageId);
      FSDefineShape3 shape = imageGenerator.defineEnclosingShape(shapeId, imageId, 0, 0, new FSSolidLine(1, FSColorTable.white()));
      
      int x = 0, y = 0;
      
      if(!isChecking)
      {
        x =  _movie.getFrameSize().getWidth() / 2 - shape.getBounds().getWidth() / 2;
        y = _movie.getFrameSize().getHeight()/2 - shape.getBounds().getHeight()/2;
      }
      
      FSPlaceObject2 placeObject = new FSPlaceObject2(shapeId, myLayer, x, y);
      
      checkBounds(shape.getBounds());      
      
      if (isChecking)
      {
        return;
      }
      
      // add the necessary elements to the movie
      _movie.add(image);
      _movie.add(shape);
      _movie.add(placeObject);
      
      // insert the necessary frames for this image
      insertFrames(_currentAction.propertyOf("duration"));
      
      // delete the image from the movie
      _movie.add (new FSRemoveObject (placeObject.getIdentifier (), myLayer));
      _movie.add (new FSRemoveObject (shape.getIdentifier (), myLayer));
      _movie.add (new FSRemoveObject (image.getIdentifier (), myLayer));
      
      // after deleting this image from the movie, there might be labels still visible, must eliminate them too
      closePendingObjects();
      
      Logger.add("Added image:["+ imageFilename +"]");
      
    } catch (Exception e) {
      throw new RuntimeException("Unable to process image ["+ imageFilename +"]",e);
    }
  }
  
  
  private void checkBounds(FSBounds newBounds)
  {
    _maxBounds = FlashWriter.merge(_maxBounds, newBounds);
  }
  
  /**
   * Adds a single line text frame with no background.
   * The slide will be shown for SLIDE_DURATION seconds 
   * @param text the text to show
   */
  private void insertTextFrame(String text, boolean isChecking)
  {
    ArrayList<String> multiText = new ArrayList<String>();
    multiText.add(text);
    insertMultiLineTextFrame(multiText, isChecking);    
  }
  
  /**
   * Adds a multiline text frame with no background. 
   * The slide will be shown for SLIDE_DURATION seconds 
   */
  private void insertMultiLineTextFrame(ArrayList<String> multiText, boolean isChecking)
  {
    int fontSize = fontSize(true);
    int lineSpacing = fontSize;
    int margin = fontSize/2;
    
    FSTextConstructor textGenerator = createFont(_movie,FONT_FACE);
    FSColor textColor = ColorTranslator.getColor(_currentAction.propertyOf("color"), "white");
    FSDefineText2 text = textGenerator.defineTextBlock(_movie.newIdentifier(),multiText, fontSize, textColor, lineSpacing);      
    FSDefineFont2 font = textGenerator.defineFont();    
    
    String position = _currentAction.propertyOf("position");

    // determine text position and insert it in a place object command
    // todo: handle text more carefully. adjust font size and/or line numbers when text goes out of boundaries.
    int x = Position.calculateX(text, _movie, position, margin, Position.MIDDLE);    
    int y = Position.calculateY(text, _movie, position, margin, Position.MIDDLE);
    FSPlaceObject2 placeObject = new FSPlaceObject2(text.getIdentifier(), IMAGE_LAYER, x, y);
    
    int screenWidth = text.getBounds().getWidth() + 2*margin;
    int screenHeight = text.getBounds().getHeight() + 2*margin;
    checkBounds(new FSBounds(0, 0, screenWidth, screenHeight));
    
    if (isChecking)
    {
      return;
    }
    
    // add the necessary elements to the movie
    _movie.add(font);
    _movie.add(text);
    _movie.add(placeObject);
    
    // insert the necessary frames for this text slide
    insertFrames(_currentAction.propertyOf("duration"));
    
    // delete the text from the movie
    _movie.add (new FSRemoveObject (placeObject.getIdentifier (), IMAGE_LAYER));
    _movie.add (new FSRemoveObject (text.getIdentifier (), IMAGE_LAYER));
    _movie.add (new FSRemoveObject (font.getIdentifier (), IMAGE_LAYER));
    
    // after deleting this text from the movie, there might be labels still visible, must eliminate them too    
    closePendingObjects();
    
    if (multiText.size() == 1)
    {
      String input = multiText.get(0);
      Logger.add("Added text:["+ input +"]");
    }
    else
    {
      Logger.add("Added "+ multiText.size() +" texts");
      for (int i=0; i<multiText.size(); i++)
      {
        Logger.add("Added text:["+ multiText.get(i) +"]");  
      }
    }
  }
  
  
  /**
   * Inserts the frames necessary to let the previous slide show for 
   * SLIDE_DURATION seconds 
   */
  private void insertFrames(String duration)
  {
    for(int i=0; i<(slideDuration(duration)*FRAME_RATE); i++)
    {
      _movie.add(new FSShowFrame());
    }
  }

  /**
   * Adds a single line label to the last inserted image 
   */
  private void overlayLabel(String label, boolean isChecking)
  {
    ArrayList<String> multiLabel = new ArrayList<String>();
    multiLabel.add(label);
    overlayMultiLabel(multiLabel, isChecking);
  }

  /**
   * Adds a multiple line label to the last inserted image 
   */
  private void overlayMultiLabel(ArrayList<String> multiLabel, boolean isChecking)
  {
    // make sure the layer is above the image layer that will be added
    int myLayer = LABEL_LAYER;
  
    int fontSize = fontSize(false);
    int lineSpacing = fontSize;
    int margin = fontSize/2;
  
    FSTextConstructor textGenerator = createFont(_movie, FONT_FACE);
    FSColor textColor = ColorTranslator.getColor(_currentAction.propertyOf("color"), "white");    
    FSDefineText2 text = textGenerator.defineTextBlock(_movie.newIdentifier(), multiLabel, fontSize, textColor, lineSpacing);
    FSDefineFont2 font = textGenerator.defineFont();
  
    String position = _currentAction.propertyOf("position");
    
    // determine text position and insert it in a place object command
    // todo: handle text more carefully. adjust font size and/or line
    // numbers when text goes out of boundaries.
    int x = Position.calculateX(text, _movie, position, margin, Position.BOTTOM);    
    int y = Position.calculateY(text, _movie, position, margin, Position.BOTTOM);
    FSPlaceObject2 placeObject = new FSPlaceObject2(text.getIdentifier(), myLayer, x, y);
  
    int screenWidth = text.getBounds().getWidth() + 2*margin;
    int screenHeight = text.getBounds().getHeight() + 2*margin;
    checkBounds(new FSBounds(0, 0, screenWidth, screenHeight));
  
    if (isChecking)
    {
      return;
    }
    
    // add the necessary elements to the movie
    _movie.add(font);
    _movie.add(text);
    _movie.add(placeObject);
    
    pendingObjects.add(new FSRemoveObject(font.getIdentifier(), myLayer));
    pendingObjects.add(new FSRemoveObject(text.getIdentifier(), myLayer));
    pendingObjects.add(new FSRemoveObject(placeObject.getIdentifier(), myLayer));
    
    
    if (multiLabel.size() == 1)
    {
      String input = multiLabel.get(0);
      Logger.add("Added label:[" + input + "]");
    }
    else
    {
      Logger.add("Added " + multiLabel.size() + " labels");
      for (int i = 0; i < multiLabel.size(); i++)
      {
        Logger.add("Added label:[" + multiLabel.get(i) + "]");
      }
    }
  }
  
  /**
   * Adds a play command to the movie, which will start the sound
   * referred by filepath
   * @param filepath path to the audio file that will be played
   */
  @SuppressWarnings("unchecked")
  private void addPlay(String filepath)
  {
    try
    {
      FSSoundConstructor constructor = new FSSoundConstructor(filepath);
      int soundId = _movie.newIdentifier();

      FSDefineSound defSound = constructor.defineSound(soundId);
      FSSound sound = new FSSound(soundId, FSSound.Continue);
      sound.setLoopCount(1);

      // todo: can the sound be faded in and out?
      _movie.add(defSound);
      _movie.add(new FSStartSound(sound));
      
      // Add this sound to the list of playing tracks on the first position
      currentAudioTracks.add(0,new Integer(soundId));
    } 
    catch (Exception e)
    {
      System.err.println("Could not load file " + filepath);
      System.err.println("Presentation will be created without it");
      e.printStackTrace();
    }
  }

  /**
   * Adds a stop command to the movie, that will end the background audio
   * that might be playing. If none are being played, nothing is done.
   */
  private void addStop()
  {
    if (!currentAudioTracks.isEmpty())
    {
      Integer soundId = (Integer)currentAudioTracks.remove(0); 
      _movie.add(new FSStartSound(new FSSound(soundId.intValue(),FSSound.Stop)));      
    }
  }

  /**
   * Adds as many stop commands as current audio tracks there are.
   * That will end the background audio completely
   * If none are being played, nothing is done.
   */
  private void addStopAll()
  {
    while(!currentAudioTracks.isEmpty())
    {
      Integer soundId = (Integer)currentAudioTracks.remove(0); 
      _movie.add(new FSStartSound(new FSSound(soundId.intValue(),FSSound.Stop)));      
    }
  }

  /**
   * Eliminates from the movie the pending open objects
   */
  private void closePendingObjects()
  {
    // insert remove object commands
    for (int i = 0; i < pendingObjects.size(); i++)
    {
        _movie.add(pendingObjects.get(i));
    }
    pendingObjects.clear();
  }
  
  private FSTextConstructor createFont(FSMovie movie, String fontName)
  {
    try
    {
      return new FSTextConstructor(movie.newIdentifier(), fontName);
    } 
    catch (Exception e)
    {
      throw new RuntimeException("Unable to create font " + fontName, e);
    }
  }
  
  private int slideDuration(String duration)
  {
    return FlashAnalyzer.slideDuration(duration, DEFAULT_SLIDE_DURATION);
  }
  
  private void setMovieBounds(FlashAction[] allActions)
  {
    inspectMovie(allActions,true);
  }
  
  private void insertMovieElements(FlashAction[] allActions)
  {
    inspectMovie(allActions,false);
  }
  
  @SuppressWarnings("unchecked")
  private void inspectMovie(FlashAction[] allActions, boolean isChecking)
  {
    
    for(int i=0; i<allActions.length; i++)
    {
      
      _currentAction = allActions[i];
      
      // check if there's a background color declaration
      if(!isChecking)
      {
        setBackground(_currentAction.propertyOf(BACKGROUND_COLOR_LABEL));
      }    

      if (_currentAction.Type == FlashActionType.Image)
      {
        insertImageFrame((String)_currentAction.Data, isChecking);          
      }
      
      if (isChecking)
      {
        continue;
      }
      
      if (_currentAction.Type == FlashActionType.Text || _currentAction.Type == FlashActionType.Unknown)
      {
        insertTextFrame((String)_currentAction.Data, isChecking);          
      }
      else if (_currentAction.Type == FlashActionType.MultiText)
      {
        insertMultiLineTextFrame((ArrayList<String>)_currentAction.Data, isChecking);          
      }
      else if (_currentAction.Type == FlashActionType.Label)
      {
        overlayLabel((String)_currentAction.Data, isChecking);          
      }
      else if (_currentAction.Type == FlashActionType.MultiLabel)
      {
        overlayMultiLabel((ArrayList<String>)_currentAction.Data, isChecking);          
      }
      
      if (_currentAction.Type == FlashActionType.AudioPlay)
      {
        addPlay((String)_currentAction.Data);
      }
      else if (_currentAction.Type == FlashActionType.AudioStop)
      {
        addStop();
      }
    }
    
    if (isChecking)
    {
      _maxBounds = _maxBounds == null ? new FSBounds(0,0,MOVIE_WIDTH,MOVIE_HEIGHT) : _maxBounds;
      _movie.setFrameSize(_maxBounds);
    }
  }

  private void setBackground(String bgColorProp)
  {
    // Only insert a change if needed
    if(bgColorProp.equals(_currentBgColor))
    {
      return;
    }
    
    _movie.add(new FSRemoveObject(_activeBackground, BACKGROUND_LAYER));
    
    // null or empty declarations set the color to default
    if(bgColorProp==null || bgColorProp.length() == 0)
    {
      _activeBackground = drawRectangle(0, 0, _movie.getFrameSize().getWidth(), _movie.getFrameSize().getHeight(), 
          ColorTranslator.getColor(DEFAULT_BACKGROUND_COLOR, DEFAULT_BACKGROUND_COLOR), BACKGROUND_LAYER);
    }
    else
    {
      _activeBackground = drawRectangle(0, 0, _movie.getFrameSize().getWidth(), _movie.getFrameSize().getHeight(), 
          ColorTranslator.getColor(bgColorProp, DEFAULT_BACKGROUND_COLOR), BACKGROUND_LAYER);
    }    
    
  }

  private int fontSize(boolean isTextNotLabel) 
  {
    if (isTextNotLabel)
    {
      return TEXT_FONT_SIZE;
      //return (int)(FlashWriter.textFontToBoundsRatio * (_maxBounds.getWidth()));
    }
    else
    {
      return LABEL_FONT_SIZE;
      //return (int)(FlashWriter.labelFontToBoundsRatio * (_maxBounds.getWidth()));
    }
  }
  
  private int drawRectangle(int x, int y, int width, int height, FSColor color, int layer)
  {
     // draw a rectangle
     FSShapeConstructor path = new FSShapeConstructor();

     // Define the origin for the shape so the bottom left corner of the 
     // rectangle will appear at the coordinates the shape is place at 
     // using the FSPlaceObject or FSPlaceObject2 class.
     
     int cornerRadius = 0;

     path.add(new FSSolidLine(1, color));
     path.add(new FSSolidFill(color));

     // Create a rectangle with rounded corners.  
     path.rect(x, y, width, height, cornerRadius);
  
     // Now define the shape.
     int id = _movie.newIdentifier();
     FSDefineShape2 rect = path.defineShape(id);
     
     //FSPlaceObject.
     _movie.add(rect); 
     _movie.add(new FSPlaceObject(id, layer, x-rect.getBounds().getMinX() , y-rect.getBounds().getMinY()));
     
     return id;
  }
}
